﻿using System;
using System.Collections.Generic;
using System.Text;

namespace NN
{
    public class Genome
    {
        public Genome() { }

        public Genome(int[] Layers, double mutationRate, Random Rand, char func)
        {
            layers = Layers;
            mu = mutationRate;
            nnFunction = func;
            inputLength = Layers[0];
            outputLength = Layers[Layers.Length - 1];
            rand = Rand;

            calcGenomeLength();

            genome = new int[genomeLength];
            decGenome = new int[decGenomeLength];            
            initWeightsAndBiases();
            initGenome();

            /*
            initLists();
            */
        }

        /*
        // initialize the lists
        private void initLists()
        {
            error = new List<double>();
            errorMovAvg = new List<double>();
            vError = new List<double>();
            vErrorMovAvg = new List<double>();

            aam = new List<double>();
            correlation = new List<double>();
        }*/


        // initialize the weight and bias arrays
        public void initWeightsAndBiases()
        {
            weights = new double[layers.Length - 1][][];
            biases = new double[layers.Length - 1][];

            for (int i = 0; i < weights.GetLength(0); i++)
            {
                weights[i] = new double[layers[i]][];
                for (int j = 0; j < layers[i]; j++)
                {
                    weights[i][j] = new double[layers[i + 1]];
                }
            }

            for (int i = 0; i < biases.GetLength(0); i++)
                biases[i] = new double[layers[i + 1]];
        }

        // Calculate the genome length by number of weights and biases
        private void calcGenomeLength()
        {
            // Weights between input to 1st hidden, between hiddens, and hidden to output
            //  -also adds biases
            for (int i = 0; i < layers.Length - 1; i++)
            {
                decGenomeLength += layers[i] * layers[i + 1];
                decGenomeLength += layers[i + 1];
            }

            genomeLength = decGenomeLength * chromosomeLength;
        }

        // Initialize the binary genome string and set the decimal values
        private void initGenome()
        {
            for (int i = 0; i < genomeLength; i++)
                if (rand.NextDouble() < 0.5)
                    genome[i] = 0;
                else
                    genome[i] = 1;

            binToDec();
            decToWeights();
        }

        // update the genome's fitness values by running through training set
        public void updateFitness(double[,] inputs, double[,] outputs,
                                    double[,] valInputs, double[,] valOutputs)
        {
            error = calcError(inputs, outputs);
            aam = avgAngleMeasure(nnOutputs, outputs);
            correlation = calcCorrelation(nnOutputs, outputs);
            determineFitness(error, inputs.GetLength(0));
        }

        // calculate the error by feed forwarding and comparing results to training data
        // --error is average error%/calculation
        private double calcError(double[,] inputs, double[,] outputs)
        {
            double e = 0;

            int outputSize = outputs.GetLength(1);
            int inputSize = inputs.GetLength(1);
            int dataLength = inputs.GetLength(0);

            double[] tempOut = new double[outputSize];
            double[] tempIn = new double[inputSize];

            nnOutputs = new double[dataLength, outputSize];

            lastOutputs = new Double[dataLength, outputSize];

            for (int i = 0; i < dataLength; i++)
            {
                for (int j = 0; j < inputSize; j++)
                    tempIn[j] = inputs[i, j];

                tempOut = Fprop(tempIn);

                for (int j = 0; j < outputSize; j++)
                    nnOutputs[i, j] = tempOut[j];

                double outE = 0;

                for (int j = 0; j < outputSize; j++)
                {
                    lastOutputs[i, j] = tempOut[j];
                    outE += Math.Pow(outputs[i, j] - tempOut[j], 2);
                }

                e += outE;
            }

            e *= 0.5;
            return e;
        }

        // calculate the moving average for the validation set
        private double calcValMovAvg(double error, List<double> errorList)
        {
            int n = errorList.Count;
            if (n < MOV_AVG_LENGTH)
                return error; // MOV_AVG_LENGTH;
            else
                return (errorList[n - 1] - errorList[n - MOV_AVG_LENGTH] / MOV_AVG_LENGTH + error / MOV_AVG_LENGTH);
        }

        // invert error so that low error -> high fitness, and vice versa
        private void determineFitness(double error, int scale)
        {
            error /= scale;
            fitness = Math.Pow(1 + 1 / error, FITNESS_SCALE);
        }

        // Takes decimal chromosome values, converts to binary, applies mutation parameter,
        //  then converts back to decimal and then weights
        public void mutate()
        {
            decToBin();

            for (int i = 0; i < genomeLength; i++)
                if (rand.NextDouble() < mu)
                    genome[i] = (genome[i] + 1) % 2;

            binToDec();
            decToWeights();
        }

        // Convert decimal genome values to binary genome
        public void decToBin()
        {
            int pos = 0;
            int temp, val, rem;

            for (int i = 0; i < decGenomeLength; i++)
            {
                temp = decGenome[i];

                for (int j = chromosomeLength - 1; j >= 0; j--)
                {
                    val = (int)Math.Pow(2, j);
                    rem = temp % val;

                    if (rem != temp)
                    {
                        genome[pos++] = 1;
                        temp -= val;
                    }
                    else
                    {
                        genome[pos++] = 0;
                    }
                }
            }
        }

        // Convert binary genome string to decimal genome values
        public void binToDec()
        {
            int pos = 0;
            for (int i = 0; i < decGenome.Length; i++)
            {
                decGenome[i] = 0;
                for (int j = 0; j < chromosomeLength; j++)
                    decGenome[i] += (int)Math.Pow(2, j) * genome[pos++];
            }
        }

        // Convert decimal genome values to the weight values in the network
        public void decToWeights()
        {
            int pos = 0;

            for (int i = 0; i < weights.GetLength(0); i++)
            {
                for (int j = 0; j < weights[i].GetLength(0); j++)
                    for (int k = 0; k < weights[i][j].GetLength(0); k++)
                        weights[i][j][k] = ((decGenome[pos++] - WEIGHT_SCALE) / WEIGHT_SCALE);

                for (int j = 0; j < biases[i].GetLength(0); j++)
                    biases[i][j] = (decGenome[pos++] - WEIGHT_SCALE) / WEIGHT_SCALE;
            }

        }

        // Forward propagate the results through the network
        public double[] Fprop(double[] Inputs)
        {
            double[] inputs;
            double[] outputs;
            double[] acts;

            inputs = new double[Inputs.Length];
            inputs = Inputs;

            for (int i = 0; i < weights.GetLength(0); i++)
            {
                acts = new double[inputs.GetLength(0)];
                for (int j = 0; j < inputs.GetLength(0); j++)
                    if (nnFunction == 't')
                        acts[j] = Math.Tanh(inputs[j]);
                    else if (nnFunction == 's')
                        acts[j] = 1 / (1 + Math.Exp(-inputs[j]));

                inputs = new double[weights[i][0].GetLength(0)];
                for (int j = 0; j < inputs.Length; j++)
                    inputs[j] = 0;

                for (int j = 0; j < weights[i].GetLength(0); j++)
                    for (int k = 0; k < weights[i][j].GetLength(0); k++)
                        inputs[k] += acts[j] * weights[i][j][k];

                for (int k = 0; k < biases[i].GetLength(0); k++)
                    inputs[k] += biases[i][k];
            }

            outputs = new double[outputLength];
            for (int j = 0; j < outputLength; j++)
            {
                outputs[j] = inputs[j];
            }

            return outputs;
        }

        // calculate the average angle measure based on two data arrays
        private double avgAngleMeasure(double[,] data1, double[,] data2)
        {
            double d, a, dt, at, rn, rd, r;
            double aam = 0;

            for (int i = 0; i < data1.GetLength(1); i++)
            {
                rn = 0;
                rd = 0;

                for (int j = 0; j < data1.GetLength(0); j++)
                {
                    dt = Math.Pow(data1[j, i], 2) + Math.Pow(data2[j, i], 2);
                    if (dt == 0.0)
                        dt = 0.1;
                    d = Math.Sqrt(dt);

                    at = Math.Abs(data1[j, i] + data2[j, i]);
                    at = at / Math.Sqrt(2 * dt);
                    if (at > 1)
                        at = 1;
                    a = Math.Acos(at);

                    rn += (d * a);
                    rd += d;
                }

                r = rn / rd;
                r = 1 - (4 * r) / Math.PI;

                if (r > 1)
                    r = 1;
                else if (r < -1)
                    r = -1;

                aam += r;
            }

            aam = aam / data1.GetLength(1);

            return aam;
        }

        private double calcCorrelation(double[,] data1, double[,] data2)
        {
            double ax, ay, xt, yt, sxx, syy, sxy, r, corr;
            corr = 0;

            for (int i = 0; i < data1.GetLength(1); i++)
            {
                // find the means of the data sets
                ax = 0;
                ay = 0;
                for (int j = 0; j < data1.GetLength(0); j++)
                {
                    ax += data1[j, i];
                    ay += data2[j, i];
                }

                ax /= data1.GetLength(0);
                ay /= data2.GetLength(0);

                // find the correlation coefficient r
                sxx = 0;
                syy = 0;
                sxy = 0;

                for (int j = 0; j < data1.GetLength(0); j++)
                {
                    xt = data1[j, i] - ax;
                    yt = data2[j, i] - ay;

                    sxx += Math.Pow(xt, 2);
                    syy += Math.Pow(yt, 2);
                    sxy += xt * yt;
                }

                if (sxx == 0.0)
                    sxx = 10000.0;
                else if (syy == 0.0)
                    syy = 10000.0;

                r = sxy / Math.Sqrt(sxx * syy);

                if (r > 1.0)
                    r = 1;
                else if (r < -1)
                    r = -1;

                corr += r;
            }

            corr /= data1.GetLength(1);

            return corr;
        }

        public void SetWeights(double[][][] w)
        {
            weights = w;
        }
        public void SetBiases(double[][] b)
        {
            biases = b;
        }

        public double[][][] GetWeights()
        {
            return (double[][][])weights.Clone();
        }
        public double[][] GetBiases()
        {
            return (double[][])biases.Clone();
        }

        public int[] genome;
        public int[] decGenome;
        public int[] layers;

        public double[,] nnOutputs;

        public double fitness;
        public double normFitness;
        public double accNormFitness;

        private int outputLength;
        private int inputLength;

        public int genomeLength;
        public int decGenomeLength;

        public double mu;

        const int chromosomeLength = 15;
        double WEIGHT_SCALE = Math.Pow(2, chromosomeLength - 1);
        double FITNESS_SCALE = 6;      // scale for error->fitness calculation
        double ERROR_SCALE = 2;
        const int MOV_AVG_LENGTH = 30;

        double[][][] weights;
        double[][] biases;

        public double[,] lastOutputs;

        public char nnFunction;

        Random rand;
        public double error;
        public double aam;
        public double correlation;
        public double vError;
    }
}